// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/views/controls/textfield/textfield_model.h"

#include <algorithm>

#include "base/logging.h"
#include "base/macros.h"
#include "base/stl_util.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "ui/base/clipboard/clipboard.h"
#include "ui/base/clipboard/scoped_clipboard_writer.h"
#include "ui/gfx/range/range.h"
#include "ui/gfx/utf16_indexing.h"

namespace views {

namespace internal {

    // Edit holds state information to undo/redo editing changes. Editing operations
    // are merged when possible, like when characters are typed in sequence. Calling
    // Commit() marks an edit as an independent operation that shouldn't be merged.
    class Edit {
    public:
        enum Type {
            INSERT_EDIT,
            DELETE_EDIT,
            REPLACE_EDIT,
        };

        virtual ~Edit() { }

        // Revert the change made by this edit in |model|.
        void Undo(TextfieldModel* model)
        {
            model->ModifyText(new_text_start_, new_text_end(),
                old_text_, old_text_start_,
                old_cursor_pos_);
        }

        // Apply the change of this edit to the |model|.
        void Redo(TextfieldModel* model)
        {
            model->ModifyText(old_text_start_, old_text_end(),
                new_text_, new_text_start_,
                new_cursor_pos_);
        }

        // Try to merge the |edit| into this edit and returns true on success. The
        // merged edit will be deleted after redo and should not be reused.
        bool Merge(const Edit* edit)
        {
            // Don't merge if previous edit is DELETE. This happens when a
            // user deletes characters then hits return. In this case, the
            // delete should be treated as separate edit that can be undone
            // and should not be merged with the replace edit.
            if (type_ != DELETE_EDIT && edit->force_merge()) {
                MergeReplace(edit);
                return true;
            }
            return mergeable() && edit->mergeable() && DoMerge(edit);
        }

        // Commits the edit and marks as un-mergeable.
        void Commit() { merge_type_ = DO_NOT_MERGE; }

    private:
        friend class InsertEdit;
        friend class ReplaceEdit;
        friend class DeleteEdit;

        Edit(Type type,
            MergeType merge_type,
            size_t old_cursor_pos,
            const base::string16& old_text,
            size_t old_text_start,
            bool delete_backward,
            size_t new_cursor_pos,
            const base::string16& new_text,
            size_t new_text_start)
            : type_(type)
            , merge_type_(merge_type)
            , old_cursor_pos_(old_cursor_pos)
            , old_text_(old_text)
            , old_text_start_(old_text_start)
            , delete_backward_(delete_backward)
            , new_cursor_pos_(new_cursor_pos)
            , new_text_(new_text)
            , new_text_start_(new_text_start)
        {
        }

        // Each type of edit provides its own specific merge implementation.
        virtual bool DoMerge(const Edit* edit) = 0;

        Type type() const { return type_; }

        // Can this edit be merged?
        bool mergeable() const { return merge_type_ == MERGEABLE; }

        // Should this edit be forcibly merged with the previous edit?
        bool force_merge() const { return merge_type_ == FORCE_MERGE; }

        // Returns the end index of the |old_text_|.
        size_t old_text_end() const { return old_text_start_ + old_text_.length(); }

        // Returns the end index of the |new_text_|.
        size_t new_text_end() const { return new_text_start_ + new_text_.length(); }

        // Merge the replace edit into the current edit. This handles the special case
        // where an omnibox autocomplete string is set after a new character is typed.
        void MergeReplace(const Edit* edit)
        {
            CHECK_EQ(REPLACE_EDIT, edit->type_);
            CHECK_EQ(0U, edit->old_text_start_);
            CHECK_EQ(0U, edit->new_text_start_);
            base::string16 old_text = edit->old_text_;
            old_text.erase(new_text_start_, new_text_.length());
            old_text.insert(old_text_start_, old_text_);
            // SetText() replaces entire text. Set |old_text_| to the entire
            // replaced text with |this| edit undone.
            old_text_ = old_text;
            old_text_start_ = edit->old_text_start_;
            delete_backward_ = false;

            new_text_ = edit->new_text_;
            new_text_start_ = edit->new_text_start_;
            merge_type_ = DO_NOT_MERGE;
        }

        Type type_;

        // True if the edit can be marged.
        MergeType merge_type_;
        // Old cursor position.
        size_t old_cursor_pos_;
        // Deleted text by this edit.
        base::string16 old_text_;
        // The index of |old_text_|.
        size_t old_text_start_;
        // True if the deletion is made backward.
        bool delete_backward_;
        // New cursor position.
        size_t new_cursor_pos_;
        // Added text.
        base::string16 new_text_;
        // The index of |new_text_|
        size_t new_text_start_;

        DISALLOW_COPY_AND_ASSIGN(Edit);
    };

    class InsertEdit : public Edit {
    public:
        InsertEdit(bool mergeable, const base::string16& new_text, size_t at)
            : Edit(INSERT_EDIT,
                mergeable ? MERGEABLE : DO_NOT_MERGE,
                at /* old cursor */,
                base::string16(),
                at,
                false /* N/A */,
                at + new_text.length() /* new cursor */,
                new_text,
                at)
        {
        }

        // Edit implementation.
        bool DoMerge(const Edit* edit) override
        {
            if (edit->type() != INSERT_EDIT || new_text_end() != edit->new_text_start_)
                return false;
            // If continuous edit, merge it.
            // TODO(oshima): gtk splits edits between whitespace. Find out what
            // we want to here and implement if necessary.
            new_text_ += edit->new_text_;
            new_cursor_pos_ = edit->new_cursor_pos_;
            return true;
        }
    };

    class ReplaceEdit : public Edit {
    public:
        ReplaceEdit(MergeType merge_type,
            const base::string16& old_text,
            size_t old_cursor_pos,
            size_t old_text_start,
            bool backward,
            size_t new_cursor_pos,
            const base::string16& new_text,
            size_t new_text_start)
            : Edit(REPLACE_EDIT, merge_type,
                old_cursor_pos,
                old_text,
                old_text_start,
                backward,
                new_cursor_pos,
                new_text,
                new_text_start)
        {
        }

        // Edit implementation.
        bool DoMerge(const Edit* edit) override
        {
            if (edit->type() == DELETE_EDIT || new_text_end() != edit->old_text_start_ || edit->old_text_start_ != edit->new_text_start_)
                return false;
            old_text_ += edit->old_text_;
            new_text_ += edit->new_text_;
            new_cursor_pos_ = edit->new_cursor_pos_;
            return true;
        }
    };

    class DeleteEdit : public Edit {
    public:
        DeleteEdit(bool mergeable,
            const base::string16& text,
            size_t text_start,
            bool backward)
            : Edit(DELETE_EDIT,
                mergeable ? MERGEABLE : DO_NOT_MERGE,
                (backward ? text_start + text.length() : text_start),
                text,
                text_start,
                backward,
                text_start,
                base::string16(),
                text_start)
        {
        }

        // Edit implementation.
        bool DoMerge(const Edit* edit) override
        {
            if (edit->type() != DELETE_EDIT)
                return false;

            if (delete_backward_) {
                // backspace can be merged only with backspace at the same position.
                if (!edit->delete_backward_ || old_text_start_ != edit->old_text_end())
                    return false;
                old_text_start_ = edit->old_text_start_;
                old_text_ = edit->old_text_ + old_text_;
                new_cursor_pos_ = edit->new_cursor_pos_;
            } else {
                // delete can be merged only with delete at the same position.
                if (edit->delete_backward_ || old_text_start_ != edit->old_text_start_)
                    return false;
                old_text_ += edit->old_text_;
            }
            return true;
        }
    };

} // namespace internal

namespace {

    // Returns the first segment that is visually emphasized. Usually it's used for
    // representing the target clause (on Windows). Returns an invalid range if
    // there is no such a range.
    gfx::Range GetFirstEmphasizedRange(const ui::CompositionText& composition)
    {
        for (size_t i = 0; i < composition.underlines.size(); ++i) {
            const ui::CompositionUnderline& underline = composition.underlines[i];
            if (underline.thick)
                return gfx::Range(underline.start_offset, underline.end_offset);
        }
        return gfx::Range::InvalidRange();
    }

} // namespace

using internal::DeleteEdit;
using internal::DO_NOT_MERGE;
using internal::Edit;
using internal::FORCE_MERGE;
using internal::InsertEdit;
using internal::MERGEABLE;
using internal::MergeType;
using internal::ReplaceEdit;

/////////////////////////////////////////////////////////////////
// TextfieldModel: public

TextfieldModel::Delegate::~Delegate() { }

TextfieldModel::TextfieldModel(Delegate* delegate)
    : delegate_(delegate)
    , render_text_(gfx::RenderText::CreateInstanceForEditing())
    , current_edit_(edit_history_.end())
{
}

TextfieldModel::~TextfieldModel()
{
    ClearEditHistory();
    ClearComposition();
}

bool TextfieldModel::SetText(const base::string16& new_text)
{
    bool changed = false;
    if (HasCompositionText()) {
        ConfirmCompositionText();
        changed = true;
    }
    if (text() != new_text) {
        if (changed) // No need to remember composition.
            Undo();
        size_t old_cursor = GetCursorPosition();
        // SetText moves the cursor to the end.
        size_t new_cursor = new_text.length();
        SelectAll(false);
        // If there is a composition text, don't merge with previous edit.
        // Otherwise, force merge the edits.
        ExecuteAndRecordReplace(changed ? DO_NOT_MERGE : FORCE_MERGE,
            old_cursor, new_cursor, new_text, 0U);
        render_text_->SetCursorPosition(new_cursor);
    }
    ClearSelection();
    return changed;
}

void TextfieldModel::Append(const base::string16& new_text)
{
    if (HasCompositionText())
        ConfirmCompositionText();
    size_t save = GetCursorPosition();
    MoveCursor(gfx::LINE_BREAK,
        render_text_->GetVisualDirectionOfLogicalEnd(),
        false);
    InsertText(new_text);
    render_text_->SetCursorPosition(save);
    ClearSelection();
}

bool TextfieldModel::Delete()
{
    if (HasCompositionText()) {
        // No undo/redo for composition text.
        CancelCompositionText();
        return true;
    }
    if (HasSelection()) {
        DeleteSelection();
        return true;
    }
    if (text().length() > GetCursorPosition()) {
        size_t cursor_position = GetCursorPosition();
        size_t next_grapheme_index = render_text_->IndexOfAdjacentGrapheme(
            cursor_position, gfx::CURSOR_FORWARD);
        ExecuteAndRecordDelete(gfx::Range(cursor_position, next_grapheme_index),
            true);
        return true;
    }
    return false;
}

bool TextfieldModel::Backspace()
{
    if (HasCompositionText()) {
        // No undo/redo for composition text.
        CancelCompositionText();
        return true;
    }
    if (HasSelection()) {
        DeleteSelection();
        return true;
    }
    size_t cursor_position = GetCursorPosition();
    if (cursor_position > 0) {
        // Delete one code point, which may be two UTF-16 words.
        size_t previous_char = gfx::UTF16OffsetToIndex(text(), cursor_position, -1);
        ExecuteAndRecordDelete(gfx::Range(cursor_position, previous_char), true);
        return true;
    }
    return false;
}

size_t TextfieldModel::GetCursorPosition() const
{
    return render_text_->cursor_position();
}

void TextfieldModel::MoveCursor(gfx::BreakType break_type,
    gfx::VisualCursorDirection direction,
    bool select)
{
    if (HasCompositionText())
        ConfirmCompositionText();
    render_text_->MoveCursor(break_type, direction, select);
}

bool TextfieldModel::MoveCursorTo(const gfx::SelectionModel& cursor)
{
    if (HasCompositionText()) {
        ConfirmCompositionText();
        // ConfirmCompositionText() updates cursor position. Need to reflect it in
        // the SelectionModel parameter of MoveCursorTo().
        gfx::Range range(render_text_->selection().start(), cursor.caret_pos());
        if (!range.is_empty())
            return render_text_->SelectRange(range);
        return render_text_->MoveCursorTo(
            gfx::SelectionModel(cursor.caret_pos(), cursor.caret_affinity()));
    }
    return render_text_->MoveCursorTo(cursor);
}

bool TextfieldModel::MoveCursorTo(const gfx::Point& point, bool select)
{
    if (HasCompositionText())
        ConfirmCompositionText();
    gfx::SelectionModel cursor = render_text_->FindCursorPosition(point);
    if (select)
        cursor.set_selection_start(render_text_->selection().start());
    return render_text_->MoveCursorTo(cursor);
}

base::string16 TextfieldModel::GetSelectedText() const
{
    return text().substr(render_text_->selection().GetMin(),
        render_text_->selection().length());
}

void TextfieldModel::SelectRange(const gfx::Range& range)
{
    if (HasCompositionText())
        ConfirmCompositionText();
    render_text_->SelectRange(range);
}

void TextfieldModel::SelectSelectionModel(const gfx::SelectionModel& sel)
{
    if (HasCompositionText())
        ConfirmCompositionText();
    render_text_->MoveCursorTo(sel);
}

void TextfieldModel::SelectAll(bool reversed)
{
    if (HasCompositionText())
        ConfirmCompositionText();
    render_text_->SelectAll(reversed);
}

void TextfieldModel::SelectWord()
{
    if (HasCompositionText())
        ConfirmCompositionText();
    render_text_->SelectWord();
}

void TextfieldModel::ClearSelection()
{
    if (HasCompositionText())
        ConfirmCompositionText();
    render_text_->ClearSelection();
}

bool TextfieldModel::CanUndo()
{
    return edit_history_.size() && current_edit_ != edit_history_.end();
}

bool TextfieldModel::CanRedo()
{
    if (!edit_history_.size())
        return false;
    // There is no redo iff the current edit is the last element in the history.
    EditHistory::iterator iter = current_edit_;
    return iter == edit_history_.end() || // at the top.
        ++iter != edit_history_.end();
}

bool TextfieldModel::Undo()
{
    if (!CanUndo())
        return false;
    DCHECK(!HasCompositionText());
    if (HasCompositionText())
        CancelCompositionText();

    base::string16 old = text();
    size_t old_cursor = GetCursorPosition();
    (*current_edit_)->Commit();
    (*current_edit_)->Undo(this);

    if (current_edit_ == edit_history_.begin())
        current_edit_ = edit_history_.end();
    else
        current_edit_--;
    return old != text() || old_cursor != GetCursorPosition();
}

bool TextfieldModel::Redo()
{
    if (!CanRedo())
        return false;
    DCHECK(!HasCompositionText());
    if (HasCompositionText())
        CancelCompositionText();

    if (current_edit_ == edit_history_.end())
        current_edit_ = edit_history_.begin();
    else
        current_edit_++;
    base::string16 old = text();
    size_t old_cursor = GetCursorPosition();
    (*current_edit_)->Redo(this);
    return old != text() || old_cursor != GetCursorPosition();
}

bool TextfieldModel::Cut()
{
    if (!HasCompositionText() && HasSelection() && !render_text_->obscured()) {
        ui::ScopedClipboardWriter(
            ui::CLIPBOARD_TYPE_COPY_PASTE)
            .WriteText(GetSelectedText());
        // A trick to let undo/redo handle cursor correctly.
        // Undoing CUT moves the cursor to the end of the change rather
        // than beginning, unlike Delete/Backspace.
        // TODO(oshima): Change Delete/Backspace to use DeleteSelection,
        // update DeleteEdit and remove this trick.
        const gfx::Range& selection = render_text_->selection();
        render_text_->SelectRange(gfx::Range(selection.end(), selection.start()));
        DeleteSelection();
        return true;
    }
    return false;
}

bool TextfieldModel::Copy()
{
    if (!HasCompositionText() && HasSelection() && !render_text_->obscured()) {
        ui::ScopedClipboardWriter(
            ui::CLIPBOARD_TYPE_COPY_PASTE)
            .WriteText(GetSelectedText());
        return true;
    }
    return false;
}

bool TextfieldModel::Paste()
{
    base::string16 text;
    ui::Clipboard::GetForCurrentThread()->ReadText(ui::CLIPBOARD_TYPE_COPY_PASTE,
        &text);
    if (text.empty())
        return false;

    base::string16 actual_text = base::CollapseWhitespace(text, false);
    // If the clipboard contains all whitespaces then paste a single space.
    if (actual_text.empty())
        actual_text = base::ASCIIToUTF16(" ");

    InsertTextInternal(actual_text, false);
    return true;
}

bool TextfieldModel::HasSelection() const
{
    return !render_text_->selection().is_empty();
}

void TextfieldModel::DeleteSelection()
{
    DCHECK(!HasCompositionText());
    DCHECK(HasSelection());
    ExecuteAndRecordDelete(render_text_->selection(), false);
}

void TextfieldModel::DeleteSelectionAndInsertTextAt(
    const base::string16& new_text,
    size_t position)
{
    if (HasCompositionText())
        CancelCompositionText();
    ExecuteAndRecordReplace(DO_NOT_MERGE,
        GetCursorPosition(),
        position + new_text.length(),
        new_text,
        position);
}

base::string16 TextfieldModel::GetTextFromRange(const gfx::Range& range) const
{
    if (range.IsValid() && range.GetMin() < text().length())
        return text().substr(range.GetMin(), range.length());
    return base::string16();
}

void TextfieldModel::GetTextRange(gfx::Range* range) const
{
    *range = gfx::Range(0, text().length());
}

void TextfieldModel::SetCompositionText(
    const ui::CompositionText& composition)
{
    if (HasCompositionText())
        CancelCompositionText();
    else if (HasSelection())
        DeleteSelection();

    if (composition.text.empty())
        return;

    size_t cursor = GetCursorPosition();
    base::string16 new_text = text();
    render_text_->SetText(new_text.insert(cursor, composition.text));
    composition_range_ = gfx::Range(cursor, cursor + composition.text.length());
    // Don't render transparent composition underlines.
    if (composition.underlines.size() > 0 && composition.underlines[0].color != 0)
        render_text_->SetCompositionRange(composition_range_);
    else
        render_text_->SetCompositionRange(gfx::Range::InvalidRange());
    gfx::Range emphasized_range = GetFirstEmphasizedRange(composition);
    if (emphasized_range.IsValid()) {
        // This is a workaround due to the lack of support in RenderText to draw
        // a thick underline. In a composition returned from an IME, the segment
        // emphasized by a thick underline usually represents the target clause.
        // Because the target clause is more important than the actual selection
        // range (or caret position) in the composition here we use a selection-like
        // marker instead to show this range.
        // TODO(yukawa, msw): Support thick underlines and remove this workaround.
        render_text_->SelectRange(gfx::Range(
            cursor + emphasized_range.GetMin(),
            cursor + emphasized_range.GetMax()));
    } else if (!composition.selection.is_empty()) {
        render_text_->SelectRange(gfx::Range(
            cursor + composition.selection.GetMin(),
            cursor + composition.selection.GetMax()));
    } else {
        render_text_->SetCursorPosition(cursor + composition.selection.end());
    }
}

void TextfieldModel::ConfirmCompositionText()
{
    DCHECK(HasCompositionText());
    base::string16 composition = text().substr(
        composition_range_.start(), composition_range_.length());
    // TODO(oshima): current behavior on ChromeOS is a bit weird and not
    // sure exactly how this should work. Find out and fix if necessary.
    AddOrMergeEditHistory(
        new InsertEdit(false, composition, composition_range_.start()));
    render_text_->SetCursorPosition(composition_range_.end());
    ClearComposition();
    if (delegate_)
        delegate_->OnCompositionTextConfirmedOrCleared();
}

void TextfieldModel::CancelCompositionText()
{
    DCHECK(HasCompositionText());
    gfx::Range range = composition_range_;
    ClearComposition();
    base::string16 new_text = text();
    render_text_->SetText(new_text.erase(range.start(), range.length()));
    render_text_->SetCursorPosition(range.start());
    if (delegate_)
        delegate_->OnCompositionTextConfirmedOrCleared();
}

void TextfieldModel::ClearComposition()
{
    composition_range_ = gfx::Range::InvalidRange();
    render_text_->SetCompositionRange(composition_range_);
}

void TextfieldModel::GetCompositionTextRange(gfx::Range* range) const
{
    *range = composition_range_;
}

bool TextfieldModel::HasCompositionText() const
{
    return !composition_range_.is_empty();
}

void TextfieldModel::ClearEditHistory()
{
    STLDeleteElements(&edit_history_);
    current_edit_ = edit_history_.end();
}

/////////////////////////////////////////////////////////////////
// TextfieldModel: private

void TextfieldModel::InsertTextInternal(const base::string16& new_text,
    bool mergeable)
{
    if (HasCompositionText()) {
        CancelCompositionText();
        ExecuteAndRecordInsert(new_text, mergeable);
    } else if (HasSelection()) {
        ExecuteAndRecordReplaceSelection(mergeable ? MERGEABLE : DO_NOT_MERGE,
            new_text);
    } else {
        ExecuteAndRecordInsert(new_text, mergeable);
    }
}

void TextfieldModel::ReplaceTextInternal(const base::string16& new_text,
    bool mergeable)
{
    if (HasCompositionText()) {
        CancelCompositionText();
    } else if (!HasSelection()) {
        size_t cursor = GetCursorPosition();
        const gfx::SelectionModel& model = render_text_->selection_model();
        // When there is no selection, the default is to replace the next grapheme
        // with |new_text|. So, need to find the index of next grapheme first.
        size_t next = render_text_->IndexOfAdjacentGrapheme(cursor, gfx::CURSOR_FORWARD);
        if (next == model.caret_pos())
            render_text_->MoveCursorTo(model);
        else
            render_text_->SelectRange(gfx::Range(next, model.caret_pos()));
    }
    // Edit history is recorded in InsertText.
    InsertTextInternal(new_text, mergeable);
}

void TextfieldModel::ClearRedoHistory()
{
    if (edit_history_.begin() == edit_history_.end())
        return;
    if (current_edit_ == edit_history_.end()) {
        ClearEditHistory();
        return;
    }
    EditHistory::iterator delete_start = current_edit_;
    delete_start++;
    STLDeleteContainerPointers(delete_start, edit_history_.end());
    edit_history_.erase(delete_start, edit_history_.end());
}

void TextfieldModel::ExecuteAndRecordDelete(gfx::Range range, bool mergeable)
{
    size_t old_text_start = range.GetMin();
    const base::string16 old_text = text().substr(old_text_start, range.length());
    bool backward = range.is_reversed();
    Edit* edit = new DeleteEdit(mergeable, old_text, old_text_start, backward);
    bool delete_edit = AddOrMergeEditHistory(edit);
    edit->Redo(this);
    if (delete_edit)
        delete edit;
}

void TextfieldModel::ExecuteAndRecordReplaceSelection(
    MergeType merge_type,
    const base::string16& new_text)
{
    size_t new_text_start = render_text_->selection().GetMin();
    size_t new_cursor_pos = new_text_start + new_text.length();
    ExecuteAndRecordReplace(merge_type,
        GetCursorPosition(),
        new_cursor_pos,
        new_text,
        new_text_start);
}

void TextfieldModel::ExecuteAndRecordReplace(MergeType merge_type,
    size_t old_cursor_pos,
    size_t new_cursor_pos,
    const base::string16& new_text,
    size_t new_text_start)
{
    size_t old_text_start = render_text_->selection().GetMin();
    bool backward = render_text_->selection().is_reversed();
    Edit* edit = new ReplaceEdit(merge_type,
        GetSelectedText(),
        old_cursor_pos,
        old_text_start,
        backward,
        new_cursor_pos,
        new_text,
        new_text_start);
    bool delete_edit = AddOrMergeEditHistory(edit);
    edit->Redo(this);
    if (delete_edit)
        delete edit;
}

void TextfieldModel::ExecuteAndRecordInsert(const base::string16& new_text,
    bool mergeable)
{
    Edit* edit = new InsertEdit(mergeable, new_text, GetCursorPosition());
    bool delete_edit = AddOrMergeEditHistory(edit);
    edit->Redo(this);
    if (delete_edit)
        delete edit;
}

bool TextfieldModel::AddOrMergeEditHistory(Edit* edit)
{
    ClearRedoHistory();

    if (current_edit_ != edit_history_.end() && (*current_edit_)->Merge(edit)) {
        // If a current edit exists and has been merged with a new edit, don't add
        // to the history, and return true to delete |edit| after redo.
        return true;
    }
    edit_history_.push_back(edit);
    if (current_edit_ == edit_history_.end()) {
        // If there is no redoable edit, this is the 1st edit because RedoHistory
        // has been already deleted.
        DCHECK_EQ(1u, edit_history_.size());
        current_edit_ = edit_history_.begin();
    } else {
        current_edit_++;
    }
    return false;
}

void TextfieldModel::ModifyText(size_t delete_from,
    size_t delete_to,
    const base::string16& new_text,
    size_t new_text_insert_at,
    size_t new_cursor_pos)
{
    DCHECK_LE(delete_from, delete_to);
    base::string16 old_text = text();
    ClearComposition();
    if (delete_from != delete_to)
        render_text_->SetText(old_text.erase(delete_from, delete_to - delete_from));
    if (!new_text.empty())
        render_text_->SetText(old_text.insert(new_text_insert_at, new_text));
    render_text_->SetCursorPosition(new_cursor_pos);
    // TODO(oshima): Select text that was just undone, like Mac (but not GTK).
}

} // namespace views
